﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace Kokomo.Mirror.CliMetadata
{
	abstract class CliTypeBase : Type
	{
		protected CliTypeBase(
			CliMemberBasicInfo basicInfo
			)
		{
			this.basicInfo = basicInfo;
			if ((object)basicInfo.module == null)
				throw new ArgumentNullException("basicInfo.module");

			this._declaredMethods = new Lazy<MethodBase[]>(this.LoadDeclaredMethods);
			this._methods = new Lazy<MethodBase[]>(this.LoadMethods);
			this._declaredEvents = new Lazy<EventInfo[]>(this.LoadDeclaredEvents);
			this._events = new Lazy<EventInfo[]>(this.LoadEvents);
			this._fields = new Lazy<FieldInfo[]>(this.LoadFields);
			this._declaredFields = new Lazy<FieldInfo[]>(this.LoadDeclaredFields);
			this._properties = new Lazy<PropertyInfo[]>(this.LoadProperties);
			this._declaredProperties = new Lazy<PropertyInfo[]>(this.LoadDeclaredProperties);
			//this._nestedTypes = new Lazy<Type[]>(this.LoadNestedTypes);
			//this._declaredNestedTypes = new Lazy<Type[]>(this.LoadDeclaredNestedTypes);
		}

		public override int GetHashCode()
		{
			int hash =
				this.Module.GetHashCode()
				^ this.MetadataToken.GetHashCode()
				;
			return hash;
		}

		public override bool Equals(object o)
		{
			Type type = o as Type;
			if ((object)type == null) return false;

			bool matches =
				(this.Module.FullyQualifiedName == type.Module.FullyQualifiedName)
				&& (this.MetadataToken == type.MetadataToken)
				&& (this.FullName == type.FullName)
				;
			return matches;
		}

		#region Type info
		protected CliMemberBasicInfo basicInfo;

		public override int MetadataToken
		{
			get { return this.basicInfo.token; }
		}

		public override Type DeclaringType
		{
			get { return this.basicInfo.declaringType; }
		}

		public override Type ReflectedType
		{
			get { return this.basicInfo.reflectedType; }
		}

		public override Assembly Assembly
		{
			get { return this.Module.Assembly; }
		}

		public override Module Module
		{
			get { return this.basicInfo.module; }
		}

		protected CliModule CliModule
		{
			get { return this.basicInfo.module; }
		}

		public override string AssemblyQualifiedName
		{
			get { return (this.FullName == null) ? null : this.FullName + ", " + this.Assembly.FullName; }
		}

		public override string FullName
		{
			get
			{
				if (string.IsNullOrEmpty(this.Namespace)) return this.Name;
				return this.Namespace + "." + this.Name;
			}
		}

		public override Guid GUID
		{
			get { return new Guid(); }
		}

		protected override bool HasElementTypeImpl()
		{
			return false;
		}

		protected override bool IsArrayImpl()
		{
			return false;
		}

		protected override bool IsByRefImpl()
		{
			return false;
		}

		protected override bool IsCOMObjectImpl()
		{
			return false;
		}

		protected override bool IsPointerImpl()
		{
			return false;
		}

		protected override bool IsPrimitiveImpl()
		{
			return false;
		}

		public override Type UnderlyingSystemType
		{
			get { return this; }
		}

		public override Type GetElementType()
		{
			return null;
		}

		public override bool IsSubclassOf(Type c)
		{
			bool isSubclass = false;

			// HACK: Helps resolve assembly load errors
			Type baseType;
			try
			{
				baseType = this.BaseType;
			}
			catch
			{
				baseType = null;
			}

			while (!isSubclass && ((object)baseType != null))
			{
				isSubclass =
					(baseType.FullName == c.FullName)
					//&& (baseType.MetadataToken == c.MetadataToken)
					;
				baseType = baseType.BaseType;
			}

			return isSubclass;
		}
		protected override TypeCode GetTypeCodeImpl()
		{
			return base.GetTypeCodeImpl();
		}

		public override bool ContainsGenericParameters
		{
			get { return false; }
		}

		public override Type MakeByRefType()
		{
			return new CliByRefType(new CliMemberBasicInfo(this.CliModule, CodedIndex.NullTypeDef), this);
		}
		public override Type MakePointerType()
		{
			return new CliPointerType(new CliMemberBasicInfo(this.CliModule, CodedIndex.NullTypeDef), this);
		}
		public override Type MakeArrayType()
		{
			return new CliArrayType(new CliMemberBasicInfo(this.basicInfo.module, CodedIndex.NullTypeDef), this);
		}

		public override Type MakeArrayType(int rank)
		{
			return new CliArrayType(new CliMemberBasicInfo(this.basicInfo.module, CodedIndex.NullTypeDef), this, rank);
		}
		#endregion

		#region Members
		protected TMember[] LoadBaseMembers<TMember>(
			TMember[] declaredMembers,
			Func<CliModule, CliImplementedType, Type, TMember[]> membersDelegate
			)
		{
			List<TMember> members = new List<TMember>();
			members.AddRange(declaredMembers);

			CliImplementedType baseType = this.BaseType as CliImplementedType;
			while ((object)baseType != null)
			{
				TMember[] baseMembers = membersDelegate(baseType.CliModule, baseType, this);
				members.AddRange(baseMembers);
				baseType = baseType.BaseType as CliImplementedType;
			}

			return members.ToArray();
		}
		public override MemberInfo[] GetMembers(BindingFlags bindingAttr)
		{
			FieldInfo[] fields = this.GetFields(bindingAttr);
			MethodInfo[] methods = this.GetMethods(bindingAttr);
			PropertyInfo[] properties = this.GetProperties(bindingAttr);
			EventInfo[] events = this.GetEvents(bindingAttr);
			Type[] nestedTypes = this.GetNestedTypes(bindingAttr);
			ConstructorInfo[] constructors = this.GetConstructors(bindingAttr);

			List<MemberInfo> members = new List<MemberInfo>(
				fields.Length + methods.Length + properties.Length + events.Length + nestedTypes.Length
				);
			members.AddRange(methods);
			members.AddRange(constructors);
			members.AddRange(properties);
			members.AddRange(events);
			members.AddRange(fields);
			members.AddRange(nestedTypes);

			return members.ToArray();
		}
		public override MemberInfo[] GetMember(string name, MemberTypes type, BindingFlags bindingAttr)
		{
			List<MemberInfo> members = new List<MemberInfo>();

			if (type.HasFlag(MemberTypes.Method))
				members.AddRange(this.GetMethods(bindingAttr).Where(m => m.Name == name));
			if (type.HasFlag(MemberTypes.Constructor))
				members.AddRange(this.GetConstructors(bindingAttr));
			if (type.HasFlag(MemberTypes.Field))
				members.AddRange(this.GetFields(bindingAttr).Where(m => m.Name == name));
			if (type.HasFlag(MemberTypes.Property))
				members.AddRange(this.GetProperties(bindingAttr).Where(m => m.Name == name));
			if (type.HasFlag(MemberTypes.Event))
				members.AddRange(this.GetEvents(bindingAttr).Where(m => m.Name == name));

			return members.ToArray();
		}

		#region Methods
		private Lazy<MethodBase[]> _declaredMethods;
		protected virtual MethodBase[] LoadDeclaredMethods()
		{
			return new MethodBase[0];
		}

		private Lazy<MethodBase[]> _methods;
		private MethodBase[] LoadMethods()
		{
			return this.LoadBaseMembers(
				this._declaredMethods.Value,
				(module, declaringType, reflectedType) => module.QueryMethodsForTypeDef(declaringType, reflectedType)
				).Where(m => (m is MethodInfo) || object.ReferenceEquals(m.DeclaringType, this)).ToArray();
		}
		protected override MethodInfo GetMethodImpl(string name, BindingFlags bindingAttr, Binder binder, CallingConventions callConvention, Type[] types, ParameterModifier[] modifiers)
		{
			MethodInfo[] methods = this.GetMethods(bindingAttr).Where(m => m.Name == name).ToArray();
			if (methods.Length == 0) return null;

			if (binder == null) binder = CliBinder.Instance;
			MethodInfo method = (MethodInfo)binder.SelectMethod(
				bindingAttr,
				methods,
				types,
				modifiers
				);
			return method;
		}

		public MethodBase[] GetDeclaredMethods()
		{
			return this._declaredMethods.Value;
		}
		public override MethodInfo[] GetMethods(BindingFlags bindingAttr)
		{
			bool includeInstance = bindingAttr.HasFlag(BindingFlags.Instance);
			bool flattenHierarchy = bindingAttr.HasFlag(BindingFlags.FlattenHierarchy);
			bool includeStatic = bindingAttr.HasFlag(BindingFlags.Static);
			bool includePublic = bindingAttr.HasFlag(BindingFlags.Public);
			bool includeNonPublic = bindingAttr.HasFlag(BindingFlags.NonPublic);
			bool declaredOnly = bindingAttr.HasFlag(BindingFlags.DeclaredOnly);

			var methods = (declaredOnly ? this._declaredMethods.Value : this._methods.Value)
				.OfType<MethodInfo>();
			if (!(includeStatic && includeInstance && (declaredOnly || flattenHierarchy)))
				methods = methods.Where(method => (
					method.IsStatic
					? (includeStatic && (declaredOnly || flattenHierarchy || object.ReferenceEquals(method.DeclaringType, this)))
					: includeInstance
					//(includeInstance && !method.IsStatic)
					//|| (
					//    includeStatic
					//    && method.IsStatic
					//    && (
					//        flattenHierarchy
					//        || (object.ReferenceEquals(method.DeclaringType, this))
					//        )
					//    )
					));
			if (!(includePublic && includeNonPublic))
				methods = methods.Where(method => (
					method.IsPublic ? includePublic : includeNonPublic
					//(includePublic && method.IsPublic)
					//|| (includeNonPublic && !method.IsPublic)
					))
				;

			return methods.ToArray();
		}
		#endregion
		#region Constructors
		protected override ConstructorInfo GetConstructorImpl(BindingFlags bindingAttr, Binder binder, CallingConventions callConvention, Type[] types, ParameterModifier[] modifiers)
		{
			ConstructorInfo[] constructors = this.GetConstructors(bindingAttr);
			if (constructors.Length == 0) return null;

			if (binder == null) binder = CliBinder.Instance;
			ConstructorInfo ctor = (ConstructorInfo)binder.SelectMethod(
				bindingAttr,
				constructors,
				types,
				modifiers
				);
			return ctor;
		}

		public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr)
		{
			var constructors = this._declaredMethods.Value// (bindingAttr.HasFlag(BindingFlags.DeclaredOnly) ? this._declaredMethods.Value : this._methods.Value)
				.OfType<ConstructorInfo>()
				.Where(ctor => (
					(bindingAttr.HasFlag(BindingFlags.Instance) && !ctor.IsStatic)
					|| (
						bindingAttr.HasFlag(BindingFlags.Static)
						&& ctor.IsStatic
						&& (
							bindingAttr.HasFlag(BindingFlags.FlattenHierarchy)
							|| (ctor.DeclaringType == this)
							)
						)
					))
				.Where(ctor => (
					(bindingAttr.HasFlag(BindingFlags.Public) && ctor.IsPublic)
					|| (bindingAttr.HasFlag(BindingFlags.NonPublic) && !ctor.IsPublic)
					))
				;

			return constructors.ToArray();
		}
		#endregion
		#region Properties
		private Lazy<PropertyInfo[]> _declaredProperties;
		protected virtual PropertyInfo[] LoadDeclaredProperties()
		{
			return new PropertyInfo[0];
		}
		private Lazy<PropertyInfo[]> _properties;
		private PropertyInfo[] LoadProperties()
		{
			return this.LoadBaseMembers(
				this._declaredProperties.Value,
				(module, declaringType, reflectedType) => module.QueryPropertiesForTypeDef(declaringType, reflectedType)
				);
		}

		public override PropertyInfo[] GetProperties(BindingFlags bindingAttr)
		{
			var properties = (bindingAttr.HasFlag(BindingFlags.DeclaredOnly) ? this._declaredProperties.Value : this._properties.Value)
				.OfType<PropertyInfo>()
				.Where(p => (
					(bindingAttr.HasFlag(BindingFlags.Instance) && !p.GetAccessors(true)[0].IsStatic)
					|| (
						bindingAttr.HasFlag(BindingFlags.Static)
						&& p.GetAccessors(true)[0].IsStatic
						&& (
							bindingAttr.HasFlag(BindingFlags.FlattenHierarchy)
							|| (object.ReferenceEquals(p.DeclaringType, this))
							)
						)
					))
				.Where(p => (
					(bindingAttr.HasFlag(BindingFlags.Public) && p.GetAccessors(true)[0].IsPublic)
					|| (bindingAttr.HasFlag(BindingFlags.NonPublic) && !p.GetAccessors(true)[0].IsPublic)
					))
				;

			return properties.ToArray();
		}

		protected override PropertyInfo GetPropertyImpl(string name, BindingFlags bindingAttr, Binder binder, Type returnType, Type[] types, ParameterModifier[] modifiers)
		{
			PropertyInfo[] properties = this.GetProperties(bindingAttr).Where(p => p.Name == name).ToArray();
			if (properties.Length == 0) return null;

			if (binder == null) binder = CliBinder.Instance;
			PropertyInfo ctor = (PropertyInfo)binder.SelectProperty(
				bindingAttr,
				properties,
				returnType,
				Type.EmptyTypes,
				modifiers
				);
			return ctor;
		}
		#endregion
		#region Events
		private Lazy<EventInfo[]> _declaredEvents;
		protected virtual EventInfo[] LoadDeclaredEvents()
		{
			return new EventInfo[0];
		}
		private Lazy<EventInfo[]> _events;
		private EventInfo[] LoadEvents()
		{
			return this.LoadBaseMembers(
				this._declaredEvents.Value,
				(module, declaringType, reflectedType) => module.QueryEventsForTypeDef(declaringType, reflectedType)
				);
		}

		public override EventInfo GetEvent(string name, BindingFlags bindingAttr)
		{
			EventInfo[] events = this.GetEvents(bindingAttr);
			StringComparison comparison =
				(bindingAttr.HasFlag(BindingFlags.IgnoreCase))
				? StringComparison.InvariantCultureIgnoreCase
				: StringComparison.InvariantCulture
				;
			EventInfo eventInfo = events.FirstOrDefault(e => string.Equals(e.Name, name, comparison));
			return eventInfo;
		}

		public override EventInfo[] GetEvents(BindingFlags bindingAttr)
		{
			var events = (bindingAttr.HasFlag(BindingFlags.DeclaredOnly) ? this._declaredEvents.Value : this._events.Value)
				.OfType<EventInfo>()
				.Where(eventInfo => (
					(bindingAttr.HasFlag(BindingFlags.Instance) && !eventInfo.GetAddMethod(true).IsStatic)
					|| (
						bindingAttr.HasFlag(BindingFlags.Static)
						&& eventInfo.GetAddMethod(true).IsStatic
						&& (
							bindingAttr.HasFlag(BindingFlags.FlattenHierarchy)
							|| (object.ReferenceEquals(eventInfo.DeclaringType, this))
							)
						)
					))
				.Where(eventInfo => (
					(bindingAttr.HasFlag(BindingFlags.Public) && eventInfo.GetAddMethod(true).IsPublic)
					|| (bindingAttr.HasFlag(BindingFlags.NonPublic) && !eventInfo.GetAddMethod(true).IsPublic)
					))
				;

			return events.ToArray();
		}

		#endregion
		#region Fields
		private Lazy<FieldInfo[]> _declaredFields;
		private Lazy<FieldInfo[]> _fields;

		protected virtual FieldInfo[] LoadDeclaredFields()
		{
			return new FieldInfo[0];
		}
		private FieldInfo[] LoadFields()
		{
			return this.LoadBaseMembers(
				this._declaredFields.Value,
				(module, declaringType, reflectedType) => module.QueryFieldsForTypeDef(declaringType, reflectedType)
				);
		}

		public override FieldInfo GetField(string name, BindingFlags bindingAttr)
		{
			FieldInfo[] fields = this.GetFields(bindingAttr);
			if (fields.Length == 0) return null;

			bool ignoreCase = bindingAttr.HasFlag(BindingFlags.IgnoreCase);
			StringComparison comparison = ignoreCase
				? StringComparison.InvariantCultureIgnoreCase
				: StringComparison.InvariantCulture
				;

			FieldInfo field = fields.FirstOrDefault(f => string.Equals(name, f.Name, comparison));
			return field;
		}

		public override FieldInfo[] GetFields(BindingFlags bindingAttr)
		{
			var fields = (bindingAttr.HasFlag(BindingFlags.DeclaredOnly) ? this._declaredFields.Value : this._fields.Value)
				.OfType<FieldInfo>()
				.Where(f => (
					(bindingAttr.HasFlag(BindingFlags.Instance) && !f.IsStatic)
					|| (
						bindingAttr.HasFlag(BindingFlags.Static)
						&& f.IsStatic
						&& (
							bindingAttr.HasFlag(BindingFlags.FlattenHierarchy)
							|| (object.ReferenceEquals(f.DeclaringType, this))
							)
						)
					))
				.Where(f => (
					(bindingAttr.HasFlag(BindingFlags.Public) && f.IsPublic)
					|| (bindingAttr.HasFlag(BindingFlags.NonPublic) && !f.IsPublic)
					))
				;

			return fields.ToArray();
		}
		#region Nested types
		//private Lazy<Type[]> _declaredNestedTypes;
		protected virtual Type[] LoadDeclaredNestedTypes()
		{
			return new Type[0];
		}
		//private Lazy<Type[]> _nestedTypes;
		private Type[] LoadNestedTypes()
		{
			return this.LoadBaseMembers(
				//this._declaredNestedTypes.Value,
				this.LoadDeclaredNestedTypes(),
				(module, declaringType, reflectedType) => module.QueryNestedTypesForTypeDef(declaringType, reflectedType)
				);
		}

		public override Type GetNestedType(string name, System.Reflection.BindingFlags bindingAttr)
		{
			Type[] nestedTypes = this.GetNestedTypes(bindingAttr);
			if (nestedTypes.Length == 0) return null;

			bool ignoreCase = bindingAttr.HasFlag(BindingFlags.IgnoreCase);
			StringComparison comparison = ignoreCase
				? StringComparison.InvariantCultureIgnoreCase
				: StringComparison.InvariantCulture
				;

			Type nestedType = nestedTypes.FirstOrDefault(t => string.Equals(name, t.Name, comparison));
			return nestedType;
		}

		public override Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr)
		{
			//var fields = (bindingAttr.HasFlag(BindingFlags.DeclaredOnly) ? this._declaredNestedTypes.Value : this._nestedTypes.Value)
			var fields = (bindingAttr.HasFlag(BindingFlags.DeclaredOnly) ? this.LoadDeclaredNestedTypes() : this.LoadNestedTypes())
				.OfType<Type>()
				//.Where(ctor => (
				//    (bindingAttr.HasFlag(BindingFlags.Instance) && !ctor.IsStatic)
				//    || (bindingAttr.HasFlag(BindingFlags.Static) && ctor.IsStatic)
				//    ))
				.Where(t => (
					(bindingAttr.HasFlag(BindingFlags.Public) && t.IsPublic)
					|| (bindingAttr.HasFlag(BindingFlags.NonPublic) && !t.IsPublic)
					))
				;

			return fields.ToArray();
		}
		#endregion
		#endregion

		#region Interfaces
		public override Type[] GetInterfaces()
		{
			return new Type[0];
		}

		#endregion

		public override Type GetInterface(string name, bool ignoreCase)
		{
			return null;
		}

		public override object InvokeMember(string name, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, object target, object[] args, System.Reflection.ParameterModifier[] modifiers, System.Globalization.CultureInfo culture, string[] namedParameters)
		{
			throw new NotImplementedException();
		}

		#endregion
		#region Attributes
		public override object[] GetCustomAttributes(Type attributeType, bool inherit)
		{
			throw new NotImplementedException();
		}

		public override object[] GetCustomAttributes(bool inherit)
		{
			throw new NotImplementedException();
		}

		public override bool IsDefined(Type attributeType, bool inherit)
		{
			return this.CliModule.IsAttributeDefinedOn(this.MetadataToken, attributeType, inherit);
		}
		#endregion

		public abstract Type Reflect();
	}
}
